﻿/**
 * Licensed under the MIT License
 *
 * Copyright (c) 2010 Alexandre Croiseaux
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */
package marcel.media.video
{
	import flash.display.Shape;
	import flash.events.AsyncErrorEvent;
	import flash.events.ErrorEvent;
	import flash.events.NetStatusEvent;
	import flash.events.SecurityErrorEvent;
	import flash.filters.GlowFilter;
	import flash.media.SoundTransform;
	import flash.media.Video;
	import flash.net.NetConnection;
	import flash.net.NetStream;
	import flash.text.TextField;
	import flash.text.TextFormat;
	import marcel.debug.*;
	import marcel.display.View;
	import marcel.events.VideoEvent;
	import marcel.utils.Framer;
	
	
	/**
	* @eventType marcel.events.VideoEvent.START
	*/
	[Event(name="onVideoStart", type="marcel.events.VideoEvent")]
	/**
	* @eventType marcel.events.VideoEvent.SEEK
	*/
	[Event(name="onVideoSeek", type="marcel.events.VideoEvent")]
	/**
	* @eventType marcel.events.VideoEvent.STOP
	*/
	[Event(name="onVideoStop", type="marcel.events.VideoEvent")]
	/**
	* @eventType marcel.events.VideoEvent.END
	*/
	[Event(name="onVideoEnd", type="marcel.events.VideoEvent")]
	/**
	* @eventType marcel.events.VideoEvent.LOOP
	*/
	[Event(name="onVideoLoop", type="marcel.events.VideoEvent")]
	/**
	* @eventType marcel.events.VideoEvent.RESIZE
	*/
	[Event(name="onVideoResize", type="marcel.events.VideoEvent")]
	/**
	* @eventType marcel.events.VideoEvent.BUFFERING
	*/
	[Event(name="onVideoBuffering", type="marcel.events.VideoEvent")]
	/**
	* @eventType marcel.events.VideoEvent.PLAYING
	*/
	[Event(name="onVideoPlaying", type="marcel.events.VideoEvent")]
	/**
	* @eventType marcel.events.VideoEvent.ERROR
	*/
	[Event(name="onVideoError", type="marcel.events.VideoEvent")]
	/**
	* @eventType marcel.events.VideoEvent.METADATA
	*/
	[Event(name="onVideoMetaData", type="marcel.events.VideoEvent")]
	/**
	* @eventType marcel.events.VideoEvent.LOAD_COMPLETE
	*/
	[Event(name="onVideoLoadComplete", type="marcel.events.VideoEvent")]
	/**
	* @eventType marcel.events.VideoEvent.STATUS
	*/
	[Event(name="onVideoStatus", type="marcel.events.VideoEvent")]
	/**
	* @eventType marcel.events.VideoEvent.CUE_POINT
	*/
	[Event(name="onVideoCuePoint", type="marcel.events.VideoEvent")]
	/**
	* @eventType marcel.events.VideoEvent.LOADING
	*/
	[Event(name="onVideoLoading", type="marcel.events.VideoEvent")]
	/**
	* @eventType marcel.events.VideoEvent.PAUSE
	*/
	[Event(name="onVideoPause", type="marcel.events.VideoEvent")]
	
	
	/**
	 * VideoDisplay class used to play video file
	 * @author Alexandre Croiseaux
	 * @see		flash.net.NetStream
	 */
	public class VideoDisplay extends View
	{
		//--------------------------------------------------------------------------
		//
		//  Public vars
		//
		//--------------------------------------------------------------------------
		/**
		 * Indicates how long to buffer messages before starting to display the stream. For example, to make sure that the first 15 seconds of the stream play without interruption, set bufferTime to 15; the application begins playing the stream only after 15 seconds of data are buffered.
		 */
		public var bufferTime:Number = 1;
		/**
		 * Indicates if the stream must restarted from the begining when the playhead reaches the end of the file
		 */
		public var loop:Boolean = false;
		/**
		 * Indicates a seek point to go when the video starts. !!Use only for debug purpose!!
		 */
		public var debugStartTime:Number = 0;
		/**
		 * Indicates if the video object should be automatically resized to the size of the video file
		 */
		public var autoSize:Boolean = false;

		//--------------------------------------------------------------------------
		//
		//  Private vars
		//
		//--------------------------------------------------------------------------
		private var _nc:NetConnection;
		private var _ns:NetStream;
		private var _framer:Framer;
		private var _infos_txt:TextField;
		private var _video:Video;
		private var _loaded:Boolean;
		private var _paused:Boolean;
		private var _ended:Boolean;
		private var _stopped:Boolean;
		private var _subtitles:VideoSubtitles;
		private var _metaData:Object;
		private var _duration:Number;
		private var _bg:Shape;
		private var _url:String;
		
		
		//--------------------------------------------------------------------------
		//
		//  Constructor
		//
		//--------------------------------------------------------------------------
		/**
		 * Constructs a new VideoDisplay instance
		 * @param	width	the width of the inner video object
		 * @param	height	the height of the inner video object
		 * @param	bgColor	The color of the background when no video is played
		 * @param	bgAlpha	The alpha of the background when no video is played
		 * @see		flash.net.NetStream
		 */
		public function VideoDisplay(width:int = 320, height:int = 240, bgColor:uint = 0x000000, bgAlpha:Number = 1.0)
		{
			_framer = new Framer(checkBuffer);
			addEventListener(VideoEvent.METADATA, onMetaData);
			
			_nc = new NetConnection();
			_nc.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
			_nc.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onError);
			_nc.connect(null);
			
			_ns = new NetStream(_nc);
			_ns.client = new VideoClient(this);
			_ns.addEventListener(NetStatusEvent.NET_STATUS, onStatus);
			_ns.addEventListener(AsyncErrorEvent.ASYNC_ERROR, onError);
			
			_bg = new Shape();
			_bg.graphics.beginFill(bgColor, bgAlpha);
			_bg.graphics.drawRect(0, 0, 100, 100);
			_bg.width = width;
			_bg.height = height;
			addChild(_bg);
			
			_video = new Video(width, height);
			_video.smoothing = true;
			_video.attachNetStream(_ns);
			addChild(_video);
			
			_infos_txt = new TextField();
			_infos_txt.defaultTextFormat = new TextFormat("Arial", 10, 0xFFFFFF, true);
			_infos_txt.wordWrap = true;
			_infos_txt.width = 250;
			_infos_txt.selectable = false;
			_infos_txt.filters = [new GlowFilter(0, 1, 3, 3, 20, 1)];
			_infos_txt.visible = false;
			setInfos("Stop");
			addChild(_infos_txt);
		}
		
		
		//--------------------------------------------------------------------------
		//
		//  Public methods
		//
		//--------------------------------------------------------------------------
		/**
		 * Specifies the video file that should be played. Use the play() method to start playing the video.
		 * @param	videoURL	The URL of the video file.
		 */
		public function setVideoURL(videoURL:String):void
		{
			_url = videoURL;
			stop();
			_ns.bufferTime = bufferTime;
			setInfos("URL set: " + _url);
		}
		
		/**
		 * Returns the current video file.
		 * @return	The URL of the video file being played.
		 */
		public function getVideoURL():String
		{
			return _url;
		}
		
		/**
		 * Opens a netstream to the URL specified with the setVideoURL() mathod and starts loading the video file, the video starts playing when the buffer is full.
		 * If the neststream is already opened and the video is paused, the video will only be resumed.
		 * If the video has reached the end of the stream, calling play() will restart the video from the beginning.
		 */
		override public function play():void
		{
			if (!_url)
			{
				stop();
				setInfos("No URL set. Please use the 'setVideoURL' method.");
				debugw("No URL set. Please use the 'setVideoURL' method.");
			}
			else
			{
				if (_stopped && (_subtitles == null || _subtitles.isReady()))
				{
					_ns.play(_url);
					_stopped = false;
				}
				else if (_paused)
				{
					if (_ended)
					{
						seek(0);
						_ended = false;
					}
					_paused = false;
					_ns.resume();
					dispatchEvent(new VideoEvent(VideoEvent.PAUSE, _paused));
				}
				
				_framer.start();
			}
		}
		
		/**
		 * Stops playing the current video and closes the associated netstream.
		 */
		override public function stop():void
		{
			_framer.stop();
			_ns.close();
			_framer.setCallback(checkBuffer);
			_video.clear();
			_stopped = true;
			_loaded = false;
			_paused = false;
			_ended = false;
			setInfos("Stop (stream closed)");
			dispatchEvent(new VideoEvent(VideoEvent.STOP));
		}
		
		/**
		 * Fixes the video size to the specified values. This sets the autosize property to false.
		 * @param	width	the width of the video
		 * @param	height	the height of the video
		 */
		public function setVideoSize(width:uint, height:uint):void
		{
			_video.width = width;
			_video.height = height;
			_bg.width = width;
			_bg.height = height;
			autoSize = false;
			dispatchEvent(new VideoEvent(VideoEvent.RESIZE, {width:width, height:height}));
		}
		
		/**
		 * Fixes the video size to the specified values and keep the good ratio. This sets the autosize property to false.
		 * @param	originalWidth  original width of the video
		 * @param	originalHeight  original height of the video
		 * @param	containerWidth  width of the container of the video
		 * @param	containerHeight  height of the container of the video
		 */
		public function setVideoSizeRatio(originalWidth:uint, originalHeight:uint, containerWidth:uint, containerHeight:uint, mustFullfillContainer:Boolean = false):void
		{
			var func:Function = mustFullfillContainer ? Math.max : Math.min
			var ratio:Number = func(containerWidth / originalWidth, containerHeight / originalHeight);
			
			setVideoSize(originalWidth * ratio, originalHeight * ratio);
		}
		
		/**
		 * Indicates the subtitles (SRT format) to use with this video
		 * @param	vs	a VideoSubtitles instance containing SRT data
		 */
		public function setSubtitles(vs:VideoSubtitles):void
		{
			_subtitles = vs;
			_subtitles.setDisplayer(this);
			_subtitles.addEventListener(VideoEvent.SUBTITLES_READY, onSubtitlesReady);
		}
		
		/**
		 * The duration of the video, defined once the video metadata duration is recieved
		 */
		public function get duration():Number { return isNaN(_duration) ? 0 : _duration; }
		
		/**
		 * The metadata object of the video file being played
		 */
		public function get metaData():Object { return _metaData; }
		
		/**
		 * Checks if the video infos are visible (buffer, loading, playhead)
		 * @return	true if infos are visible, false otherwise
		 */
		public function isInfosVisible():Boolean { return _infos_txt.visible; }
		
		/**
		 * Sets the video infos visible (buffer, loading, playhead)
		 */
		public function setInfosVisible(bVisible:Boolean):void { _infos_txt.visible = bVisible; }
		
		/**
		 * Get the video percent played (range [0-1])
		 * @return the video percent played (range [0-1])
		 */
		public function getPrcPlayed():Number
		{
			if (isNaN(_ns.time) || duration == 0) return 0;
			else return _ns.time / duration;
		}
		
		/**
		 * Get the video percent loaded (range [0-1])
		 * @return the video percent loaded (range [0-1])
		 */
		public function getPrcLoaded():Number
		{
			if (isNaN(_ns.bytesTotal) || isNaN(_ns.bytesLoaded)) return 0;
			else return _ns.bytesLoaded / _ns.bytesTotal;
		}
		
		/**
		 * Indicates the volume of the video being played (range[0-1])
		 * @param	volume	the volume of the video (range[0-1])
		 */
		public function setVolume(volume:Number):void
		{
			var st:SoundTransform = _ns.soundTransform;
			st.volume = volume;
			_ns.soundTransform = st;
		}
		
		/**
		 * Get the volume of the video being played (range[0-1])
		 * @return	volume of the video being played (range[0-1])
		 */
		public function getVolume():Number { return _ns.soundTransform.volume; }
		
		/**
		 * Seeks the keyframe (also called an I-frame in the video industry) closest to the specified location. The keyframe is placed at an offset, in seconds, from the beginning of the stream.
		 * @param	offset	The approximate time value, in seconds, to move to in a video file. With Flash Media Server, if EnhancedSeek is set to true in the Application.xml configuration file (which it is by default), the server generates a keyframe at offset.
		 * @see		flash.net.NetStream
		 */
		public function seek(offset:Number):void
		{
			_ended = false;
			_ns.seek(offset);
			dispatchEvent(new VideoEvent(VideoEvent.SEEK, offset));
		}
		
		/**
		 * Pauses playback of a video stream.
		 */
		public function pause():void { _paused = true; _ns.pause(); dispatchEvent(new VideoEvent(VideoEvent.PAUSE, _paused)); }
		
		/**
		 * Returns the inner Video object
		 * @return	the Video instance used to display the stream
		 */
		public function getVideo():Video { return _video; }
		
		/**
		 * The position of the playhead, in seconds.
		 * @return	The position of the playhead, in seconds.
		 */
		public function getTime():Number { return _ns.time; }
		
		/**
		 * Checks is the video is paused or not
		 * @return	true if the video stream is paused, false otherwise
		 */
		public function isPaused():Boolean { return _paused; }
		
		/**
		 * Checks is the video is playing or not
		 * @return	true if the video stream is playing, false otherwise
		 */
		public function isPlaying():Boolean { return !_stopped && !_paused; }
		
		
		//--------------------------------------------------------------------------
		//
		//  Private methods
		//
		//--------------------------------------------------------------------------
		private function checkBuffer():void
		{
			displayInfos();
			if(_ns.bufferLength >= _ns.bufferTime)
			{
				_framer.setCallback(checkPlayHead);
				dispatchEvent(new VideoEvent(VideoEvent.START));
			}
			else
			{
				var prc:int = Math.floor(_ns.bufferLength / _ns.bufferTime * 100);
				dispatchEvent(new VideoEvent(VideoEvent.BUFFERING, prc));
				dispatchEvent(new VideoEvent(VideoEvent.LOADING, getPrcLoaded()));
			}
		}
		
		private function checkPlayHead():void
		{
			displayInfos();
			
			if (autoSize && _video.width != _video.videoWidth && _video.videoWidth != 0)
			{
				_video.width = _video.videoWidth;
				_video.height = _video.videoHeight;
				_bg.width = _video.videoWidth;
				_bg.height = _video.videoHeight;
				dispatchEvent(new VideoEvent(VideoEvent.RESIZE, {width:_video.width, height:_video.height}));
			}
			
			if (!isPaused()) dispatchEvent(new VideoEvent(VideoEvent.PLAYING, getPrcPlayed()));
			if (!_loaded) dispatchEvent(new VideoEvent(VideoEvent.LOADING, getPrcLoaded()));
			
			if (getPrcLoaded() >= 1 && !_loaded)
			{
				_loaded = true;
				dispatchEvent(new VideoEvent(VideoEvent.LOAD_COMPLETE));
			}
		}
		
		private function displayInfos():void
		{
			if (isInfosVisible())
			{
				var prc:int = Math.floor(_ns.bufferLength / _ns.bufferTime * 100);
				_infos_txt.text = "Buffered " + getFormatedTime(_ns.bufferLength) + "/" + _ns.bufferTime + " seconds (" + prc + "%)";
				_infos_txt.appendText("\nPlayed " + getFormatedTime(_ns.time) + "/" + duration + " seconds (" + Math.round(getPrcPlayed()*100) + "%)");
				_infos_txt.appendText("\nLoaded " + _ns.bytesLoaded + "/" + _ns.bytesTotal + " octets (" + Math.round(getPrcLoaded()*100) + "%)");
				if (_ended) _infos_txt.appendText("\nEnd");
				else if (_paused) _infos_txt.appendText("\nPaused");
			}
		}
		
		private function getFormatedTime(time:Number):String
		{
			var strTime:String = Math.round(time * 100).toString();
			if (strTime.length < 3) strTime = "0." + strTime;
			else strTime = strTime.substr(0, strTime.length-2) + "." + strTime.substr(strTime.length-2);
			return strTime;
		}
		
		private function setInfos(infos:String):void { _infos_txt.text = infos; }
		private function appendInfos(infos:String):void { _infos_txt.appendText("\n" + infos); }
		
		
		//--------------------------------------------------------------------------
		//
		//  Events
		//
		//--------------------------------------------------------------------------
		private function onStatus(evt:NetStatusEvent):void
		{
			switch (evt.info.code)
			{
				case "NetStream.Play.Start":
					appendInfos("Stream found => waiting to MetaData")
					break;
				case "NetStream.Play.StreamNotFound":
					stop();
					setInfos("Stream Not found => " + _url);
					break;
				case "NetStream.Play.Stop":
				case "NetStream.Buffer.Flush":
				case "NetStream.Buffer.Empty":
					if (!_stopped && !_ended && _duration > 0 && Math.abs(duration - _ns.time) <= .1)
					{
						_ended = true;
						if (loop)
						{
							dispatchEvent(new VideoEvent(VideoEvent.LOOP));
							seek(0);
						}
						else
						{
							pause();
							dispatchEvent(new VideoEvent(VideoEvent.END));
						}
					}
					break;
			}
			dispatchEvent(new VideoEvent(VideoEvent.STATUS, evt.info.code));
		}
		
		private function onError(evt:ErrorEvent):void
		{
			dispatchEvent(new VideoEvent(VideoEvent.ERROR, evt.text));
		}
		
		private function onSubtitlesReady(evt:VideoEvent):void
		{
			if (_url != null) _ns.play(_url);
		}
		
		private function onMetaData(evt:VideoEvent):void
		{
			_metaData = evt.value;
			_duration = evt.value.duration;
			if (debugStartTime > 0) seek(debugStartTime);
		}
	}
}




// Private VideoClient class
import marcel.debug.*;
import marcel.events.VideoEvent;
import marcel.media.video.VideoDisplay;

internal class VideoClient
{
	private var _videoDisplay:VideoDisplay;
	
	public function VideoClient(video:VideoDisplay)
	{
		_videoDisplay = video;
	}
	
	public function onMetaData(info:Object):void
	{
		//Logger.logi(this, "onMetaData: duration=" + info.duration + " width=" + info.width + " height=" + info.height + " framerate=" + info.framerate);
		_videoDisplay.dispatchEvent(new VideoEvent(VideoEvent.METADATA, info));
	}
	
	public function onXMPData(info:Object):void
	{
		//Logger.logi(this, "onXMPData: duration=" + info.duration + " width=" + info.width + " height=" + info.height + " framerate=" + info.framerate);
		//_videoDisplay.dispatchEvent(new VideoEvent(VideoEvent.METADATA, info));
	}

	public function onCuePoint(info:Object):void
	{
		//for (var key:String in info) Logger.logi(this, key + " = " + info[key]);
		_videoDisplay.dispatchEvent(new VideoEvent(VideoEvent.CUE_POINT, info));
	}
}
